package com.onlylemi.mapview.library.utils;


import com.onlylemi.mapview.library.utils.math.FloydAlgorithm;
import com.onlylemi.mapview.library.utils.math.GeneticAlgorithm;
import com.onlylemi.mapview.library.utils.math.TSPNearestNeighbour;
import ohos.agp.utils.Point;

import java.util.ArrayList;
import java.util.List;

/**
 * MapMath
 *
 * @author onlylemi
 */
public final class MapMath {

    private MapMath() {
    }

    /**
     * the distance between two points
     *
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @return sqrt
     */
    public static float getDistanceBetweenTwoPoints(float x1, float y1,
                                                    float x2, float y2) {
        return (float) Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    }

    /**
     * the distance between two points
     *
     * @param start
     * @param end
     * @return sqrt
     */
    public static float getDistanceBetweenTwoPoints(Point start, Point end) {
        return (float) Math.sqrt(Math.pow(end.getPointX() - start.getPointX(), 2)
                + Math.pow(end.getPointY() - start.getPointY(), 2));
    }


    /**
     * the shortest path between two points (FloydAlgorithm)
     *
     * @param begin
     * @param end
     * @param matrix adjacency matrix
     * @return findCheapestPath
     */
    public static List<Integer> getShortestPathBetweenTwoPoints(int begin,
                                                                int end, float[][] matrix) {
        return FloydAlgorithm.getInstance().findCheapestPath(begin, end, matrix);
    }

    /**
     * the best path between some points (NearestNeighbour tsp)
     *
     * @param matrix adjacency matrix
     * @return tsp
     */
    public static List<Integer> getBestPathBetweenPointsByNearestNeighbour(float[][] matrix) {
        return TSPNearestNeighbour.getInstance().tsp(matrix);
    }

    /**
     * the best path between some points (GeneticAlgorithm tsp)
     *
     * @param matrix
     * @return result
     */
    public static List<Integer> getBestPathBetweenPointsByGeneticAlgorithm(float[][] matrix) {
        GeneticAlgorithm ga = GeneticAlgorithm.getInstance();
        ga.setAutoNextGeneration(true);
        ga.setMaxGeneration(200);
        int[] best = ga.tsp(matrix);

        List<Integer> result = new ArrayList<>(best.length);
        for (int i = 0; i < best.length; i++) {
            result.add(best[i]);
        }
        return result;
    }


    /**
     * get the angle between two points and the horizontal plane
     *
     * @param start
     * @param end
     * @return angle
     */
    public static float getDegreeBetweenTwoPointsWithHorizontal(Point start, Point end) {
        float angle = 90.0f;
        if (start.getPointX() != end.getPointX()) {
            angle = (float) Math.toDegrees(Math.atan((end.getPointY() - start.getPointY())
                    / (end.getPointX() - start.getPointX())));
            if (end.getPointX() < start.getPointX() && end.getPointY() >= start.getPointY()) {
                angle = angle + 180.0f;
            } else if (end.getPointX() < start.getPointX() && end.getPointY() > start.getPointY()) {
                angle = angle - 180.f;
            }
        } else {
            if (start.getPointY() < end.getPointY()) {
                angle = 90.0f;
            } else if (start.getPointY() > end.getPointY()) {
                angle = -90.0f;
            }
        }
        return angle;
    }

    /**
     * get the angle between two points and the vertical plane
     *
     * @param start
     * @param end
     * @return angle
     */
    public static float getDegreeBetweenTwoPointsWithVertical(Point start, Point end) {
        float angle = 90.0f;
        if (start.getPointY() != end.getPointY()) {
            angle = -(float) Math.toDegrees(Math.atan((end.getPointX() - start.getPointX())
                    / (end.getPointY() - start.getPointY())));
            if (end.getPointY() > start.getPointY() && end.getPointX() >= start.getPointX()) {
                angle = angle + 180.0f;
            } else if (end.getPointY() > start.getPointY() && end.getPointX() > start.getPointX()) {
                angle = angle - 180.f;
            }
        } else {
            if (start.getPointX() < end.getPointX()) {
                angle = 90.0f;
            } else if (start.getPointX() > end.getPointX()) {
                angle = -90.0f;
            }
        }
        return angle;
    }

    /**
     * get degree between two points
     *
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @return toDegrees
     */
    public static float getDegreeBetweenTwoPoints(float x1, float y1, float x2, float y2) {
        double radians = Math.atan2(y1 - y2, x1 - x2);
        return (float) Math.toDegrees(radians);
    }

    /**
     * get degree between two points
     *
     * @param start
     * @param end
     * @return getDegreeBetweenTwoPoints
     */
    public static float getDegreeBetweenTwoPoints(Point start, Point end) {
        return getDegreeBetweenTwoPoints(start.getPointX(), start.getPointY(), end.getPointX(), end.getPointY());
    }

    /**
     * The coordinates of the midpoint between two points are obtained
     *
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @return point
     */
    public static Point getMidPointBetweenTwoPoints(float x1, float y1, float x2, float y2) {
        return new Point((x1 + x2) / 2, (y1 + y2) / 2);
    }

    /**
     * The coordinates of the midpoint between two points are obtained
     *
     * @param start
     * @param end
     * @return getMidPointBetweenTwoPoints
     */
    public static Point getMidPointBetweenTwoPoints(Point start, Point end) {
        return getMidPointBetweenTwoPoints(start.getPointX(), start.getPointY(), end.getPointX(), end.getPointY());
    }

    /**
     * Get the coordinates of any point between two points
     *
     * @param start
     * @param end
     * @param value
     * @return point
     */
    public static Point getEveryPointBetweenTwoPoints(Point start, Point end, float value) {
        // y=kx+b
        float x, y;
        // with slope
        if (start.getPointX() != end.getPointX()) {
            float k = (end.getPointY() - start.getPointY()) / (end.getPointX() - start.getPointX());
            float b = end.getPointY() - k * end.getPointX();

            if (end.getPointX() > start.getPointX()) {
                x = Math.min(end.getPointX(), start.getPointX()) + (end.getPointX() - start.getPointX()) * value;
            } else {
                x = Math.max(end.getPointX(), start.getPointX()) + (end.getPointX() - start.getPointX()) * value;
            }
            y = k * x + b;
        } else { // no slope
            x = start.getPointX();
            if (end.getPointY() > start.getPointY()) {
                y = Math.min(end.getPointY(), start.getPointY()) + (end.getPointY() - start.getPointY()) * value;
            } else {
                y = Math.max(end.getPointY(), start.getPointY()) + (end.getPointY() - start.getPointY()) * value;
            }
        }
        return new Point(x, y);
    }


    /**
     * Get a shortest distance from point to line
     *
     * @param point
     * @param linePoint1 Determine the first point of a straight line
     * @param linePoint2 Determine the second point of a straight line
     * @return float
     */
    public static float getDistanceFromPointToLine(Point point, Point linePoint1, Point
            linePoint2) {
        // y = kx + b;
        // d = |kx-y+b| / √(k^2+1)
        float d;
        if (linePoint1.getPointX() != linePoint2.getPointX()) { // with slope
            float k = (linePoint2.getPointY() - linePoint1.getPointY())
                    / (linePoint2.getPointX() - linePoint1.getPointX());
            float b = linePoint2.getPointY() - k * linePoint2.getPointX();
            d = Math.abs(k * point.getPointX() - point.getPointY() + b) / (float) Math.sqrt(k * k + 1);
        } else { // no slope
            d = Math.abs(point.getPointX() - linePoint1.getPointX());
        }
        return d;
    }

    /**
     * get intersection coordinates from a point to a line
     *
     * @param point
     * @param linePoint1
     * @param linePoint2
     * @return point
     */
    public static Point getIntersectionCoordinatesFromPointToLine(Point point, Point linePoint1, Point
            linePoint2) {
        // y = kx + b;
        float x, y;
        if (linePoint1.getPointX() != linePoint2.getPointX()) { // with slope
            float k = (linePoint2.getPointY() - linePoint1.getPointY())
                    / (linePoint2.getPointX() - linePoint1.getPointX());
            float b = linePoint2.getPointY() - k * linePoint2.getPointX();

            // The equation of point
            if (k != 0) {
                float kV = -1 / k;
                float bV = point.getPointY() - kV * point.getPointX();
                x = (b - bV) / (kV - k);
                y = kV * x + bV;
            } else {
                x = point.getPointX();
                y = linePoint1.getPointY();
            }
        } else { // no slope
            x = linePoint1.getPointX();
            y = point.getPointY();
        }
        return new Point(x, y);
    }

    /**
     * is/not obtuse angle between a point and a line
     *
     * @param point
     * @param linePoint1
     * @param linePoint2
     * @return boolean
     */
    public static boolean isObtuseAnglePointAndLine(Point point, Point linePoint1, Point
            linePoint2) {

        // A*A + B*B < C*C
        float p_l1;
        float p_l2;
        float l1_l2;
        p_l1 = getDistanceBetweenTwoPoints(point, linePoint1);
        p_l2 = getDistanceBetweenTwoPoints(point, linePoint2);
        l1_l2 = getDistanceBetweenTwoPoints(linePoint1, linePoint2);

        return ((p_l1 * p_l1 + l1_l2 * l1_l2) < p_l2 * p_l2)
                || ((p_l2 * p_l2 + l1_l2 * l1_l2) < p_l1 * p_l1);
    }

}
